#include "XmlGenerator.h"
typedef cblp::XmlGenerator XML;

#include "RoQuery.h"
#include "RoModule.h"

#include "ScriptComponent.h"


QScriptValue RoStanzaIdToScriptValue(QScriptEngine *, RoStanzaId const & in)
{
    return in.toString();
}

void RoStanzaIdFromScriptValue(const QScriptValue & object, RoStanzaId & out)
{
    out = RoStanzaId(object.toString());
}


template <class Q>
QScriptValue QObjectToScriptValue(QScriptEngine * engine, Q const & _in)
{
    Q * in = new Q(_in);
    return engine->newQObject(in, QScriptEngine::AutoOwnership);
}

template <class Q>
void QObjectFromScriptValue(const QScriptValue & object, Q & out)
{
    out = *qobject_cast<Q*>(object.toQObject());
}


QScriptValue RoIqStanza_scriptConstructor(QScriptContext * context, QScriptEngine * engine)
{
    return engine->toScriptValue(
        RoIqStanza(
            qscriptvalue_cast<QDomDocument>(
                context->argument(0)
            ) // если что-то не срастётся, получится пустой документ, а это допустимо
        )
    );
}


QScriptValue ScriptComponent::DomDocumentFromScriptValue(QScriptContext * context, QScriptEngine * engine)
{
    QDomDocument doc;
    QString strValue = context->argument(0).toString();
    if ( strValue.contains('<') )
        doc.setContent(strValue);
    else
        doc.appendChild(doc.createElement(strValue));
    return engine->toScriptValue(doc);
}


ScriptComponent::ScriptComponent(RoModule * module, const QString & _scriptFileName):
    super(module),
    result(false),
    scriptFileName(QFileInfo(_scriptFileName).canonicalFilePath()),
    qsSelf(engine.newQObject(this))
{
    setName(QString("script://%1").arg(scriptFileName));

    // заменяем глобальный объект собой
    {
        QScriptValue originalGlobalObject = engine.globalObject();
        QScriptValueIterator it(originalGlobalObject);
        while ( it.hasNext() )
        {
            it.next();
            qsSelf.setProperty(it.scriptName(), it.value(), it.flags());
        }
        engine.setGlobalObject(qsSelf);
    }

    // импорт расширений для прикидывания в кускрипт базовых компонентов Qt
    engine.importExtension("qt.core");
    engine.importExtension("qt.xml");
    Q_ASSERT_X( not engine.hasUncaughtException(),
                Q_FUNC_INFO,
                engine.uncaughtException().toString().toUtf8() );

    qScriptRegisterMetaType(&engine, RoStanzaIdToScriptValue, RoStanzaIdFromScriptValue);

    qsSelf.setPrototype(engine.newQMetaObject(&staticMetaObject));

    engine.globalObject().setProperty("XML", engine.newFunction(DomDocumentFromScriptValue, 1));

    QScriptValue qsRoStanza = engine.newQMetaObject( &RoStanza::staticMetaObject );
    engine.globalObject().setProperty("RoStanza", qsRoStanza);
    qScriptRegisterMetaType(&engine, QObjectToScriptValue<RoStanza>, QObjectFromScriptValue<RoStanza>);

    QScriptValue qsRoIqStanza =
        engine.newQMetaObject( &RoIqStanza::staticMetaObject,
                               engine.newFunction(RoIqStanza_scriptConstructor) );
    engine.globalObject().setProperty("RoIqStanza", qsRoIqStanza);
    qScriptRegisterMetaType(&engine, QObjectToScriptValue<RoIqStanza>, QObjectFromScriptValue<RoIqStanza>);

    // почему-то классовые (static) методы не прокидываются в кускрипт через класс,
    // но они по-прежнему доступны через объект,
    // и мы можем это подставить такой вот костыль
    {
        QScriptValue theRoIqStanza = qsRoIqStanza.construct();
        QStringList staticMethods = QStringList()
            << "createDatum"
            << "createError"
            << "createResult"
        ;
        foreach ( QString m, staticMethods )
            qsRoIqStanza.setProperty(m, theRoIqStanza.property(m));
    }
}


void ScriptComponent::runScript()
{
    result = false; // be pessimistic

    QFile scriptFile(scriptFileName);
    scriptFile.open(QIODevice::ReadOnly);
    engine.evaluate(scriptFile.readAll(), scriptFileName);
    scriptFile.close();

    if ( engine.hasUncaughtException() )
    {
        qDebug() << "EXCEPTION! in line" << engine.uncaughtExceptionLineNumber()
                 << "\n*** content ***" << engine.uncaughtException().toString()
                 << "\n*** backtrace ***" << engine.uncaughtExceptionBacktrace();
        engine.clearExceptions();
        emit scriptFinished();
    }
}


bool ScriptComponent::query(const QString & component_method, QScriptValue params)
{
    int delimeterPosition = component_method.indexOf("::");
    if ( delimeterPosition == -1 )
        return false;

    QString component = component_method.left(delimeterPosition);
    if ( component.isEmpty() )
        return false;

    QString method = component_method.mid(delimeterPosition + 2); // 2 == strlen("::")
    if ( method.isEmpty() )
        return false;

    RoQuery * q = createSimpleQuery(component, method);
    if ( not q )
        return false;

    // если что-то пойдёт не так, просто вернётся пустой объект
    QDomDocument argument = engine.fromScriptValue<QDomDocument>(params.property("argument"));

    bool execOk = q->exec(argument);
    if ( not execOk )
        return false;

    queriesWithParameters.insert(q, params);

    params.setProperty("component", component);
    params.setProperty("method", method);
    params.setProperty("rid", q->rid().toString());

    return true;
}


RoComponent::RoMethodResult ScriptComponent::ro_query_interface(RoIqStanza request, QDomDocument *answer)
{
    const uint PREFIX_LENGTH = QString(ROMASHKA_METHOD_PREFIX).length();

    // сначала запросим обычные методы-слоты
    RoMethodResult superResult = super::ro_query_interface(request, answer);
    if ( superResult == FinishedWithError )
    {
        return superResult;
    }
    else if ( superResult == NotFinishedYet )
    {
        *answer = RoIqStanza::createError(RoIqStanza::InternalError, request.from(), request.id())
                  .setErrorDescription("Метод ScriptComponent::ro_query_interface() считает странным поведение метода RoComponent::ro_query_interface()");
        return FinishedWithError;
    }

    // теперь добавим свои, созданные скриптом
    XML methodListXML(*answer);
    QScriptValueIterator it(qsSelf);
    while ( it.hasNext() )
    {
        it.next();
        QScriptValue v = it.value();

        if ( it.name().startsWith(ROMASHKA_METHOD_PREFIX) and
             v.isFunction() and
             not (qsSelf.propertyFlags(it.name()) & QScriptValue::QObjectMember) )
        {
            QString ro_name = it.name().mid(PREFIX_LENGTH);
            // romashkization
            ro_name.replace('_', '-');

            XML methodXML = XML("method")("name", ro_name);

            if ( v.property("description").isValid() )
                methodXML.appendChild(
                    XML("description")[v.property("description").toString()]
                );

            if ( v.property("example").isValid() )
                methodXML.appendChild(
                    XML("example")[v.property("example").toString()]
                );

            methodListXML.appendChild(methodXML);
        }
    }

    *answer = methodListXML.toDocument();
    return FinishedSuccessfully;
}


void ScriptComponent::query_in_start(RoIqStanza stanza)
{
    Q_ASSERT( stanza.type() == "request" );

    QString method_name = ROMASHKA_METHOD_PREFIX + stanza.method().replace('-', '_');

    QScriptValue scriptMethod = qsSelf.property(method_name);
    if ( scriptMethod.isFunction() and
         not (qsSelf.propertyFlags(method_name) & QScriptValue::QObjectMember) )
    {
        QScriptValue result = scriptMethod.call(
            qsSelf,
            QScriptValueList() << engine.newQObject(&stanza)
        );

        if ( result.isError() )
        {
            sendStanza(
                RoIqStanza::createError(RoIqStanza::InternalError, stanza.from(), stanza.id())
                .setErrorDescription( QString("Ошибка сценария в строке %1: %2")
                                      .arg(result.property("lineNumber").toInt32())
                                      .arg(result.toString()) )
            );
            return;
        }

        if ( not result.isArray() )
        {
            sendStanza(
                RoIqStanza::createError(RoIqStanza::InternalError, stanza.from(), stanza.id())
                .setErrorDescription( QString("Ошибка сценария: "
                                              "метод вернул %1, "
                                              "а должен был вернуть Array.")
                                      .arg(result.toString()) )
            );
            return;
        }

        const int result_length = result.property("length").toInt32();
        RoMethodResult mresult = static_cast<RoMethodResult>(result.property(0).toInt32());

        if ( result_length == 0 or
             not ( result.property(0).isNumber() ) or // первый член задан и принадлежит этому енуму
             not metaObject()->enumerator(metaObject()->indexOfEnumerator("RoMethodResult")).valueToKey(mresult) )
        {
            sendStanza(
                RoIqStanza::createError(RoIqStanza::InternalError, stanza.from(), stanza.id())
                .setErrorDescription( QString("Ошибка сценария: "
                                              "метод вернул [%1, ...], "
                                              "а должен был "
                                              "[component.FinishedSuccessfully, QDomDocument()], "
                                              "[component.FinishedWithError, RoIqStanza()] или "
                                              "[component.NotFinishedYet].")
                                      .arg(result.property(0).toString()) )
            );
            return;
        }

        if ( mresult == NotFinishedYet )
            return; // just do nothing

        // если FinishedWithError, то следом должна идти RoIqStanza-error
        if ( mresult == FinishedWithError )
        {
            RoIqStanza * iq = qobject_cast<RoIqStanza*>(result.property(1).toQObject());
            if ( result_length < 2 or
                 not iq or
                 iq->type() != "error" )
            {
                sendStanza(
                    RoIqStanza::createError(RoIqStanza::InternalError, stanza.from(), stanza.id())
                    .setErrorDescription( QString("Ошибка сценария: "
                                                  "после component.FinishedWithError метод вернул %1, "
                                                  "а ожидалось "
                                                  "RoIqStanza::createError().")
                                          .arg(result.property(1).toString()) )
                );
                return;
            }

            sendStanza(*iq);
            return;
        }

        // если FinishedSuccessfully, то вторым должен идти QDomDocument
        if ( mresult == FinishedSuccessfully )
        {
            if ( result_length < 2 or
                 not result.property(1).toVariant().canConvert<QDomDocument>() )
            {
                sendStanza(
                    RoIqStanza::createError(RoIqStanza::InternalError, stanza.from(), stanza.id())
                    .setErrorDescription( QString("Ошибка сценария: "
                                                  "после component.FinishedSuccessfully метод вернул %1, "
                                                  "а ожидался "
                                                  "QDomDocument().")
                                          .arg(result.property(1).toString()) )
                );
                return;
            }

            sendStanza(RoIqStanza::createResult(
                stanza.from(),
                stanza.id(),
                engine.fromScriptValue<QDomDocument>(result.property(1))
            ));
            return;
        }
    }
    else
    {
        super::query_in_start(stanza);
    }
}


void ScriptComponent::receive_query_datum(QDomDocument datum)
{
    RoQuery * q = qobject_cast<RoQuery*>(sender());
    if ( q and queriesWithParameters.contains(q) )
    {
        QScriptValue params = queriesWithParameters[q];
        QScriptValue dataAcceptor = params.property("dataAcceptor");
        if ( dataAcceptor.isFunction() )
            dataAcceptor.call( params, QScriptValueList() << engine.toScriptValue(datum) );
    }
}


void ScriptComponent::receive_query_result(bool error, QDomDocument answer)
{
    RoQuery * q = qobject_cast<RoQuery*>(sender());
    if ( q and queriesWithParameters.contains(q) )
    {
        QScriptValue params = queriesWithParameters[q];
        QScriptValue resultAcceptor = params.property("resultAcceptor");
        if ( resultAcceptor.isFunction() )
        {
            resultAcceptor.call( params, QScriptValueList() << error << engine.toScriptValue(answer) );
        }

        queriesWithParameters.remove(q);
        q->deleteLater();
    }
}
